/*
 * Copyright (c) 2013-2018 上海汇数数据. All rights reserved.
 * @(#) AbstractJsonOperator.java 2018-08-06 16:45
 */

package cn.jh.common.core.json;

import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;


/**
 * @author Fuchun
 * @since 1.0
 */
abstract class AbstractJsonOperator implements JsonOperator {

    static final OpenOption[] DEFAULT_OPEN_OPTIONS = { WRITE, CREATE, TRUNCATE_EXISTING };

    static final ImmutableClassToInstanceMap<Collection<?>> COLLECTIONS =
            new ImmutableClassToInstanceMap.Builder<Collection<?>>()
                    .put(List.class, ImmutableList.of())
                    .put(Set.class, ImmutableSet.of())
                    .build();

    private static <C extends Collection<?>> C collectionInstance(Class<C> collectionClass) {
        C c = COLLECTIONS.getInstance(collectionClass);
        if (c == null) {
            // use default constructor
            try {
                c = collectionClass.getConstructor().newInstance();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
        return c;
    }

    @SuppressWarnings("unchecked")
    private static <M extends Map<?,?>> M emptyMapInstance(Class<M> mapClass) {
        if (Map.class.isAssignableFrom(mapClass) || ImmutableMap.class.isAssignableFrom(mapClass)) {
            return (M) ImmutableMap.of();
        } else if (SortedMap.class.isAssignableFrom(mapClass) || ImmutableSortedMap.class.isAssignableFrom(mapClass)) {
            return (M) ImmutableSortedMap.of();
        } else if (TreeMap.class.isAssignableFrom(mapClass)) {
            return (M) new TreeMap<>();
        } else {
            try {
                return mapClass.getConstructor().newInstance();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    @Override
    public <T> T convert(Map<String, Object> map, Class<T> targetClass) {
        if (map == null) {
            return null;
        }
        if (map.isEmpty()) {
            try {
                Constructor<T> c = targetClass.getConstructor();
                return c.newInstance(); // just new instance
            } catch (NoSuchMethodException ex) {
                throw new IllegalArgumentException(String.format(
                        "The target class %s have no default constructor.", targetClass), ex);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
        return doConvert(map, targetClass);
    }

    protected abstract <T> T doConvert(Map<String, Object> map, Class<T> targetClass);

    @Override
    public void writeValue(File out, Object object, OpenOption... options) throws IOException {
        checkNotNull(out, "file");
        checkArgument(out.isFile(), "The file(%s) is not a file.", out);
        OpenOption[] openOptions;
        if (options == null || options.length == 0) {
            openOptions = DEFAULT_OPEN_OPTIONS;
        } else {
            openOptions = options;
        }
        writeValue(Files.newOutputStream(out.toPath(), openOptions), object);
    }

    @Override
    public <K, V, M extends Map<K, V>> M parseToMap(
            String jsonString, Class<M> mapClass, Class<K> keyClass, Class<V> valueClass) {
        if (jsonString == null || jsonString.isEmpty()) {
            return null;
        }
        if ("{}".equals(jsonString)) {
            return emptyMapInstance(mapClass);
        }
        checkArgument(keyClass != null, "keyClass must be not null");
        checkArgument(valueClass != null, "valueClass must be not null");
        return doParseToMap(jsonString, mapClass, keyClass, valueClass);
    }

    @Override
    public <K, V> Map<K, V> parseToMap(String jsonString, Type keyType, Type valueType) {
        if (jsonString == null || jsonString.isEmpty()) {
            return null;
        }
        if ("{}".equals(jsonString)) {
            return ImmutableMap.of();
        }
        checkArgument(keyType != null, "keyType must be not null");
        checkArgument(valueType != null, "valueType must be not null");
        return doParseToMap(jsonString, keyType, valueType);
    }

    @Override
    public <C extends Collection<T>, T> C parseToCollection(
            String jsonString, Class<C> listClass, Class<T> elementClass) {
        if (jsonString == null || jsonString.isEmpty()) {
            return null;
        }
        if ("[]".equals(jsonString)) {
            return collectionInstance(listClass);
        }
        checkArgument(listClass != null, "listClass must be not null");
        checkArgument(elementClass != null, "elementClass must be not null");
        return doParseToCollection(jsonString, listClass, elementClass);
    }

    @Override
    public <C extends Collection<T>, T> C parseToCollection(String jsonString, Class<C> colClass, Type elementType) {
        if (jsonString == null || jsonString.isEmpty()) {
            return null;
        }
        checkArgument(colClass != null, "colClass must be not null");
        checkArgument(elementType != null, "elementType must be not null");
        return doParseToCollection(jsonString, colClass, elementType);
    }

//    protected abstract  <C extends Collection<?>, T> C doParseToCollection(
//            String jsonString, Class<C> listClass, Class<T> elementClass);

    protected abstract  <C extends Collection<T>, T> C doParseToCollection(
            String jsonString, Class<C> listClass, Type elementType);

    protected abstract <K, V, M extends Map<K, V>> M doParseToMap(
            String jsonString, Class<M> mapClass, Class<K> keyClass, Class<V> valueClass);

    protected abstract <K, V> Map<K, V> doParseToMap(
            String jsonString, Type keyType, Type valueType);

    static String object2JsonString(Object input) {
        String targetJson;
        if (input instanceof String) {
            targetJson = (String) input;
        } else if (input instanceof byte[]) {
            targetJson = new String((byte[]) input, StandardCharsets.UTF_8);
        } else {
            targetJson = input.toString();
        }
        return targetJson;
    }

    static JsonParseException rethrow(Object input, Type type, Throwable ex) {
        String targetJson = object2JsonString(input);
        return new JsonParseException(
                String.format("The target json (%s) cannot parse to %s.", targetJson, type), ex);
    }
}
